use std::io::Write;

use anyhow::Result;
use chrono::Local;
use fudata::db;
use fudata::db::todo;
use fudata::model;
use fudata::model::kline::Klt;
use fudata::model::todo::Todo;
use fudata::model::todo::TodoParameter;
use fudata::model::todo::TodoStatus;
use fudata::model::todo::TodoType;
use log::error;
use log::info;
use log::trace;

use crate::fund::opf_value_craw;
use crate::fund::opfs_craw::update_opf_list_all_from_eastmoney;
use crate::helper;
use crate::todo::MAX_KLINE_DAYS;

use super::update_todo_with_result;

///
/// every day we update the opf list after 15:30
///
pub async fn generate_todo_crawl_opf_list() -> Result<()> {
    let start = Local::now();
    info!("generate_todo_crawl_opf_list start...");
    let todo_id = Todo::id_from(&[TodoType::CrawlOpfList.to_string().as_str()]);

    let todo_rs = todo::find(todo_id.as_str()).await;
    match &todo_rs {
        Ok(v) =>
            match v {
                Some(todo) => {
                    // if there is pending todo, then let it be or else update the plan_at to next 1 midnight
                    if todo.is_done() || todo.is_failed() {
                        let plan_at = helper::next_non_trading_day(&Local::now());
                        let todo_update = Todo {
                            plan_at: Some(plan_at),
                            status: Some(TodoStatus::Pending),
                            ..Default::default()
                        };
                        let _ = todo::upsert(todo_id.as_str(), &todo_update).await?;
                    }
                }
                None => {
                    // create a new todo, today midnight
                    let plan_at = helper::next_non_trading_day(&Local::now());
                    let todo_new = Todo {
                        name: Some("刷新OPF证券列表".to_string()),
                        type_: Some(TodoType::CrawlOpfList),
                        // updatable fields
                        plan_at: Some(plan_at),
                        status: Some(TodoStatus::Pending),
                        created_at: Some(Local::now()),
                        ..Default::default()
                    };
                    let _ = todo::upsert(todo_id.as_str(), &todo_new).await?;
                }
            }
        Err(e) => error!("{}", e),
    }
    info!(
        "generate_todo_crawl_opf_list end/ spent seconds={}",
        Local::now().signed_duration_since(start).num_seconds()
    );

    Ok(())
}

///
/// every day we update the opf history values for all opf securities
///
/// if exist history values, just do incremental update,
/// or else clean the history values from the db manually to do full update
///
pub async fn generate_todos_crawl_opf_values() -> Result<()> {
    let start = Local::now();

    info!("generate_todos_crawl_opf_values start...");

    let rs = db::security::select(
        &None,
        Some(model::security::SecurityType::OPF),
        None,
        None,
        0,
        30000
    ).await;
    match rs {
        Ok(securities) => {
            info!("generate_todos_crawl_opf_values for {} securities ...", securities.len());
            let mut i = 0;
            for security in securities {
                let security_id = &security.id_raw();
                let rs_p = get_todo_crawl_opf_values_parameters(security_id).await;
                match rs_p {
                    Ok(p) => {
                        let todo_id = Todo::id_from(&[security_id, &Klt::DAY.to_string()]);

                        let todo_rs = todo::find(todo_id.as_str()).await;
                        match todo_rs {
                            Ok(v) =>
                                match v {
                                    Some(todo) => {
                                        // if there is pending todo, then let it be or else update the plan_at to tomorrow
                                        if todo.is_done() || todo.is_failed() {
                                            let todo_update = Todo {
                                                // updatable fields
                                                plan_at: Some(
                                                    helper::now_plus_date_time(1, 15, 30, 0)
                                                ),
                                                status: Some(TodoStatus::Pending),
                                                parameters: Some(p),
                                                ..Default::default()
                                            };
                                            let _ = todo::upsert(
                                                todo_id.as_str(),
                                                &todo_update
                                            ).await?;
                                            print!("U");
                                        }
                                    }
                                    None => {
                                        let todo_new = Todo {
                                            name: Some(
                                                format!(
                                                    "刷新OPF证券{}的{}-K线历史净值",
                                                    security_id,
                                                    &Klt::DAY
                                                )
                                            ),
                                            type_: Some(TodoType::CrawlOpfValues),
                                            // updatable fields
                                            plan_at: Some(helper::now_plus_date_time(0, 0, 0, 0)),
                                            status: Some(TodoStatus::Pending),
                                            parameters: Some(p),
                                            created_at: Some(Local::now()),
                                            ..Default::default()
                                        };
                                        let _ = todo::upsert(todo_id.as_str(), &todo_new).await?;
                                        print!("N");
                                    }
                                }
                            Err(e) => error!("{}", e),
                        }
                    }
                    Err(e) => error!("{}", e),
                }
                if i % 100 == 0 {
                    print!("\n~~(opf_todo::generate_todos_crawl_opf_values[{:04}])~~", i);
                }
                print!("{}", i % 10);
                let _rs = std::io::stdout().flush(); // Flush stdout to ensure immediate output
                i += 1;
            }
        }
        Err(e) => error!("{}", e),
    }
    info!(
        "generate_todos_crawl_opf_values end/ spent seconds={} ",
        Local::now().signed_duration_since(start).num_seconds()
    );
    Ok(())
}

async fn get_todo_crawl_opf_values_parameters(security_id: &str) -> Result<Vec<TodoParameter>> {
    let rs_kl = db::kline::select(
        security_id,
        None,
        Some(&Klt::DAY),
        Some("klts DESC"),
        0,
        1
    ).await;

    match rs_kl {
        Ok(vks) => {
            let mut limit = MAX_KLINE_DAYS;
            // if exist klines, just do incremental update
            if vks.len() > 0 {
                limit = 10;
            }

            return Ok(
                [
                    TodoParameter {
                        name: "security_id".to_string(),
                        value: security_id.to_string(),
                    },
                    TodoParameter {
                        name: "limit".to_string(),
                        value: limit.to_string(),
                    },
                ].to_vec()
            );
        }
        Err(e) => {
            return Err(e);
        }
    }
}

pub async fn crawl_opf_list(worker_no: u8, todo: &Todo) {
    let rs = update_opf_list_all_from_eastmoney().await;
    let _ = update_todo_with_result(worker_no, rs, todo).await;
}

pub async fn crawl_opf_values(worker_no: u8, todo: &Todo) {
    let security = db::todo::get_security_by_parameter(todo).await;
    let limit = db::todo::get_limit_by_parameter(todo);

    let rs = match (security, limit) {
        (Ok(s), Ok(lmt)) => {
            let rs = opf_value_craw::update_opf_history_values_from_eastmoney(
                worker_no,
                &s,
                1,
                lmt
            ).await;
            trace!("Worker[{}] update_opf_history_values_from_eastmoney return back to process_todo_update_opf_values", worker_no);
            rs
        }
        (Err(e1), Err(e3)) => Err(e1.context(e3)),
        (Ok(_), Err(e3)) => Err(e3),
        (Err(e1), Ok(_)) => Err(e1),
    };

    match &rs {
        Ok(v) =>
            info!(
                "Worker[{}] process_todo_update_opf_values save records={:?}",
                worker_no,
                v.len()
            ),
        Err(e) => error!("Worker[{}] process_todo_update_opf_values error: {}", worker_no, e),
    }

    let _ = update_todo_with_result(worker_no, rs, todo).await;
}

#[cfg(test)]
mod test {
    use fudata::db;
    use fudata::model::todo;
    use fudata::model::todo::Todo;

    use crate::todo::crawl_opf_todo;

    #[tokio::test]
    async fn test_generate_todo_update_opf_list() {
        pretty_env_logger::init_timed();
        let _ = fudata::db::init().await.unwrap();

        let _ = crawl_opf_todo::generate_todo_crawl_opf_list().await;

        let todo_id = Todo::id_from(&["UpdateOpfList"]);

        let rs = db::todo::find(todo_id.as_str()).await;
        println!("rs: {:?}", rs);
    }

    #[tokio::test]
    async fn test_generate_todos_update_opf_values() {
        pretty_env_logger::init_timed();
        let _ = fudata::db::init().await.unwrap();

        let _ = crawl_opf_todo::generate_todos_crawl_opf_values().await;

        let rs = db::todo::select(
            &Some(vec![todo::TodoType::CrawlOpfValues]),
            &Some(todo::TodoStatus::Pending),
            false,
            0,
            10
        ).await;
        assert!(rs.is_ok());
        for (idx, todo) in rs.unwrap().iter().enumerate() {
            println!("todo[{idx}]: {:?}\n", todo);
        }
    }
}
